//+---------------------------------------------------------------------------
//
// Copyright (C) Microsoft Corporation, 2007
//
// File: DataContract.cs
//
// Contents: Data contract for the Delivery Service
//
// Created by: Adam Wardynski (adamwa) Feb 13 2008
//
//----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Security.Cryptography;
using System.Text;
using MRS.FrontEnd.BO;

namespace Microsoft.Reputation.Services
{
    #region Server-Client Communications Control

    [DataContract]
    [Serializable]
    public class StateToken
    {
        [DataMember]
        public byte[] Data;

        #region Non-DataContract code

        public const long NotInitializedField = -1;

        public const ushort CurrentVersion = 0x0110;

        public const int VersionNumberOffset = 0;
        public const int StateTokenLength = VersionNumberOffset + sizeof(ushort);

        public StateToken(StateToken original)
        {
            if (original == null)
            {
                throw new ArgumentNullException("original");
            }
            if (original.Data != null)
            {
                this.Data = (byte[])original.Data.Clone();
            }
        }

        public StateToken()
        {
            Data = new byte[]
            {
                // version number (ushort)
                0xFF, 0xFF,
            };
            Array.Copy(BitConverter.GetBytes(CurrentVersion), 0, Data, VersionNumberOffset, sizeof(ushort));
        }

        public bool IsValid()
        {
            if (Data == null)
            {
                return false;
            }
            try
            {
                if (Data.Length != StateTokenLength)
                {
                    return false;
                }

                ushort version = BitConverter.ToUInt16(Data, VersionNumberOffset);
                if (version != CurrentVersion)
                {
                    return false;
                }
            }
            catch (ArgumentOutOfRangeException)
            {
                return false;
            }
            return true;
        }
        #endregion
    }

    #endregion

    #region Reputation, Metadata and Object Identity

    [DataContract]
    public enum DataType
    {
        [EnumMember]
        Reputation,
        [EnumMember]
        Metadata,
    }

    [DataContract]
    [Serializable]
    public class CategoryReputation
    {
        [DataMember(IsRequired = true)]
        public uint CategoryId;
        [DataMember(IsRequired = true)]
        public byte Confidence;
        [DataMember(Name = "CategoryInfo", IsRequired = false)]
        private List<Metadata> m_categoryInfo;

        #region Non-DataContract code
        public ICollection<Metadata> CategoryInfo
        {
            get
            {
                return m_categoryInfo;
            }
            set
            {
                if (value == null)
                {
                    m_categoryInfo = null;
                }
                else
                {
                    m_categoryInfo = new List<Metadata>(value);
                }
            }
        }

        public CategoryReputation(
            uint categoryId,
            byte confidence,
            IEnumerable<Metadata> categoryInfo)
        {
            this.CategoryId = categoryId;
            this.Confidence = confidence;

            if (categoryInfo != null)
            {
                this.m_categoryInfo = new List<Metadata>(categoryInfo);
            }
        }

        //private CategoryReputation()
        //{
        //}

        #endregion
    }

    [DataContract]
    [Serializable]
    public class ReputationInfo
    {
        [DataMember(Name = "Data")]
        private List<CategoryReputation> m_data;

        #region Non-DataContract code

        public ICollection<CategoryReputation> Data
        {
            get
            {
                return m_data;
            }
            set
            {
                if (value == null)
                {
                    m_data = null;
                }
                else
                {
                    m_data = new List<CategoryReputation>(value);
                }
            }
        }

        public ReputationInfo(IEnumerable<CategoryReputation> data)
        {
            if (data != null)
            {
                m_data = new List<CategoryReputation>(data);
            }
        }

        //private ReputationInfo()
        //{
        //}
        #endregion
    }

    [DataContract]
    [Serializable]
    public class Metadata : IComparable<Metadata>
    {
        [DataMember(IsRequired = true)]
        public string Name;
        [DataMember(IsRequired = true)]
        public object Value;

        #region Non-DataContract code

        public Metadata(string name, object value)
        {
            this.Name = name;
            this.Value = value;
        }

        //private Metadata()
        //{
        //}

        public static Metadata FindMetadataInCollectionByName(
            IEnumerable<Metadata> metadataCollection,
            string metadataName)
        {
            Metadata value = null;
            
            foreach (Metadata metadata in metadataCollection)
            {
                if (String.IsNullOrEmpty(metadata.Name) == false
                    && metadata.Name.Equals(metadataName, StringComparison.InvariantCultureIgnoreCase))
                {
                    value = metadata;
                    break;
                }
            }

            return value;
        }
        #endregion        
    
        #region IComparable<Metadata> Members

        public int CompareTo(Metadata other)
        {
            if (other == null)
            {
                return 1;
            }
            return string.Compare(this.Name, other.Name, StringComparison.OrdinalIgnoreCase);
        }

        #endregion
    }

    [DataContract]
    [Serializable]
    public class MetadataInfo
    {
        [DataMember(Name = "Data")]
        private List<Metadata> m_data;

        #region Non-DataContract code

        public ICollection<Metadata> Data
        {
            get
            {
                return m_data;
            }
            set
            {
                if (value == null)
                {
                    m_data = null;
                }
                else
                {
                    m_data = new List<Metadata>(value);
                }
            }
        }

        public MetadataInfo(IEnumerable<Metadata> data)
        {
            if (data == null)
            {
                this.m_data = null;
            }
            else
            {
                this.m_data = new List<Metadata>(data);
            }
        }

        private MetadataInfo()
        {
        }

        #endregion
    }

    [DataContract]
    [Serializable]
    public class ReputationObject
    {
        [DataMember(Name = "Type", IsRequired = true)]
        public uint Type { get; set; }
        [DataMember(Name = "Guid", IsRequired = true)]
        public Guid Guid { get; set; }
        [DataMember(Name = "Version", IsRequired = true)]
        public long Version { get; set; }
        [DataMember(Name = "ExpirationMins", IsRequired = true)]
        public uint ExpirationMins { get; set; }
        [DataMember(Name = "Reputation", EmitDefaultValue = false)]
        public ReputationInfo Reputation { get; set; }
        [DataMember(Name = "Metadata", EmitDefaultValue = false)]
        public MetadataInfo Metadata { get; set; }

        public int CacheExpirationMins { get; set; }

        #region non-data contract code

        public ReputationObject(
            uint type,
            Guid guid,
            uint expirationMins,
            ReputationInfo reputationInfo,
            MetadataInfo metadataInfo,
            long objectVersion)
        {
            this.Type = type;
            this.Guid = guid;
            this.ExpirationMins = expirationMins;
            this.Reputation = reputationInfo;
            this.Metadata = metadataInfo;
            this.Version = objectVersion;
        }

        public ReputationObject(
            uint type,
            Guid guid,
            uint expirationMins,
            ReputationInfo reputationInfo,
            MetadataInfo metadataInfo,
            long objectVersion,
            int cacheExpirationMins) : this(type, guid, expirationMins, reputationInfo, metadataInfo, objectVersion)
        {
            this.CacheExpirationMins = cacheExpirationMins;
        }

        //private ReputationObject()
        //{
        //}

        //public bool HasReputation()
        //{
        //    return this.Reputation != null && this.Reputation.Data.Count > 0;
        //}

        /// <summary>
        /// Gets the Metadata value object from inside the Reputation Object
        /// by searching for the name field.  
        /// </summary>
        /// <param name="metadataName">A non-null string that represents the name
        /// of the Metadata field you want to search for.</param>
        /// <returns>The value of the Metadata if the name is found.  Null otherwise.</returns>
        public object GetMetadataValueByName(string metadataName)
        {
            if (String.IsNullOrEmpty(metadataName) 
                || this.Metadata == null 
                || this.Metadata.Data == null)
            {
                return null;
            }

            Metadata metadata = 
                Services.Metadata.FindMetadataInCollectionByName(this.Metadata.Data, metadataName);
            object value = null;

            if (metadata != null)
            {
                value = metadata.Value;
            }
            return value;
        }

        /// <summary>
        /// Try to find the matching ReputationObject in the collection if there is a match
        /// </summary>
        /// <param name="hash">SHA256 value to search for</param>
        /// <param name="repuObjects">The collection to search through</param>
        /// <returns>Matching ReputationObject or null if it could not be found</returns>
        //public static ReputationObject FindReputationObject(byte[] hash, ICollection<ReputationObject> repuObjects)
        //{
        //    if (repuObjects == null || repuObjects.Count == 0)
        //    {
        //        return null;
        //    }

        //    if (hash == null)
        //    {
        //        throw new ArgumentNullException("hash");
        //    }
        //    string inputHashAsString = Convert.ToBase64String(hash);

        //    foreach (ReputationObject repuObj in repuObjects)
        //    {
        //        byte[] repuHash = repuObj.GetMetadataValueByName("SHA256") as byte[];
        //        if (repuHash != null
        //            && inputHashAsString.Equals(Convert.ToBase64String(repuHash)))
        //        {
        //            return repuObj;
        //        }
        //    }
        //    return null;
        //}

        /// <summary>
        /// Convert the UnpackedObject that is returned by the database or cache into a 
        /// ReputationObject that can be returned to the calling client
        /// </summary>
        /// <param name="unpackedObj">The UnpackedObject from the database or cache</param>
        /// <returns>The ReputationObject representing the UnpackedObject</returns>
        internal static ReputationObject ConvertUnpackedObjectToReputationObject(UnpackedObject unpackedObj)
        {
            List<Metadata> metadataInfo = (
                from m in unpackedObj.Metadatas
                where m.Value != null
                select new Metadata(m.Key, m.Value)
                ).ToList();

            metadataInfo.Add(new Metadata("SHA256", unpackedObj.Sha256));

            List<CategoryReputation> categoryRep = new List<CategoryReputation>(
                unpackedObj.Reputations.Select(r => new CategoryReputation(
                    (uint)r.Category,
                    r.Confidence,
                    new List<Metadata>(
                        r.Metadatas.Select(m => ConvertReputationToMetadata(m)))
                    )
                ));

            ReputationObject repuObj = new ReputationObject(
                (uint)unpackedObj.Type,
                Guid.Empty,
                (uint)unpackedObj.ClientExpirationInMinutes,
                categoryRep.Count != 0 ? new ReputationInfo(categoryRep) : null,
                new MetadataInfo(metadataInfo),
                unpackedObj.Version);

            return repuObj;
        }

        private static Metadata ConvertReputationToMetadata(KeyValuePair<string, object> m)
        {
            object metadataValue = m.Value;
            if (UrlBinaryCodec.MetadataNameHostInheritance.Equals(m.Key)
                || UrlBinaryCodec.MetadataNamePathInheritance.Equals(m.Key))
            {
                if ((bool)m.Value)
                {
                    metadataValue = "1";
                }
                else
                {
                    metadataValue = "0";
                }
            }
            return new Metadata(m.Key, metadataValue);
        }

        #endregion

    }

    #endregion

    #region Request classes

    [Flags]
    public enum FilteringOptions
    {
        None = 0,
        ForcePreserveReputation = 1
    }

    [DataContract]
    [Serializable]
    public class Filter
    {
        [DataMember]
        public ICollection<DataType> RequestedData;
        [DataMember]
        public ICollection<string> RequestedMetadata;
        [DataMember]
        public ICollection<Int32> RequestedCategory;
        [DataMember]
        public long? LastStamp;

        #region non-data contract code

        public static uint IsValidRequest(Filter filter)
        {
            using (TraceUtility.TraceMethod(Tracing.Source))
            {
                if (filter == null)
                {
                    return StatusCodes.OK;
                }

                if (filter.RequestedData != null)
                {
                    List<DataType> seenDataTypes = new List<DataType>();
                    foreach (DataType dataType in filter.RequestedData)
                    {
                        if (seenDataTypes.Contains(dataType))
                        {
                            TraceUtility.TraceWarning(
                                Tracing.Source,
                                "Invalid request: requested data type {0} is provided more than once.",
                                dataType);

                            return StatusCodes.DuplicateRequestedDataType;
                        }
                        seenDataTypes.Add(dataType);
                    }
                }

                if (filter.RequestedMetadata != null)
                {
                    if (filter.RequestedMetadata.Count == 0)
                    {
                        TraceUtility.TraceWarning(
                            Tracing.Source,
                            "Invalid request: requested metadata count is not allowed to be zero. It can be either null or not empty.");
                        return StatusCodes.ZeroRequestedMetadata;
                    }
                    if (filter.RequestedData != null && !filter.RequestedData.Contains(DataType.Metadata))
                    {
                        TraceUtility.TraceWarning(
                            Tracing.Source,
                            "Invalid request: requested metadata provided, but Metadata has not been requested.");
                        return StatusCodes.MetadataNotRequested;
                    }
                }

                if (filter.RequestedCategory != null)
                {
                    if (filter.RequestedCategory.Count == 0)
                    {
                        TraceUtility.TraceWarning(
                            Tracing.Source,
                            "Invalid request: requested category count is not allowed to be zero. It can be either null or not empty.");
                        return StatusCodes.ZeroRequestedCategory;
                    }
                    if (filter.RequestedData != null && !filter.RequestedData.Contains(DataType.Reputation))
                    {
                        TraceUtility.TraceWarning(
                            Tracing.Source,
                            "Invalid request: requested category provided, but Reputation has not been requested.");
                        return StatusCodes.ReputationNotRequested;
                    }
                }

                return StatusCodes.OK;
            }
        }

        public ReputationObject FilterOut(ReputationObject rawObject, FilteringOptions options)
        {
            using (TraceUtility.TraceMethod(Tracing.Source))
            {
                if (rawObject == null)
                {
                    return null;
                }

                //
                // flags for filtering requested data types
                //
                bool isMetadataRequested =
                    RequestedData == null
                 || RequestedData.Contains(DataType.Metadata);
                
                bool isReputationRequested =
                    (options & FilteringOptions.ForcePreserveReputation) == FilteringOptions.ForcePreserveReputation
                 || RequestedData == null
                 || RequestedData.Contains(DataType.Reputation);

                //
                // Filtering metadata by requested metadata
                //
                MetadataInfo metadataInfo = rawObject.Metadata;
                if ( metadataInfo != null
                    && metadataInfo.Data != null
                    && isMetadataRequested
                    && RequestedMetadata != null)
                {
                    List<Metadata> filteredMetadata = new List<Metadata>(metadataInfo.Data.Count);
                    foreach (Metadata rawMetadata in metadataInfo.Data)
                    {
                        foreach (string requestedMetadatum in RequestedMetadata)
                        {
                            if (string.Equals(requestedMetadatum, rawMetadata.Name, StringComparison.OrdinalIgnoreCase))
                            {
                                filteredMetadata.Add(rawMetadata);
                            }
                        }
                    }
                    metadataInfo = new MetadataInfo(filteredMetadata);
                }

                //
                // Filtering reputation by requested category
                //
                ReputationInfo reputationInfo = rawObject.Reputation;
                if (reputationInfo != null
                    && reputationInfo.Data != null
                    && isReputationRequested
                    && RequestedCategory != null)
                {
                    List<CategoryReputation> filteredCategory = new List<CategoryReputation>(reputationInfo.Data.Count);
                    foreach (CategoryReputation rawCategory in reputationInfo.Data)
                    {
                        foreach (Int32 requestedCategory in RequestedCategory)
                        {
                            if (requestedCategory == rawCategory.CategoryId)
                            {
                                filteredCategory.Add(rawCategory);
                            }
                        }
                    }
                    reputationInfo = new ReputationInfo(filteredCategory);
                }

                //
                // Creating the filtered out version of object
                //
                ReputationObject filteredObject = new ReputationObject(
                    rawObject.Type,
                    rawObject.Guid,
                    rawObject.ExpirationMins,
                    isReputationRequested ? reputationInfo : null,
                    isMetadataRequested ? metadataInfo : null,
                    rawObject.Version);

                return filteredObject;
            }
        }

        public ReputationObject FilterOut(ReputationObject rawObject)
        {
            using (TraceUtility.TraceMethod(Tracing.Source))
            {
                return FilterOut(rawObject, FilteringOptions.None);
            }
        }

        public Filter(
            DataType[] requestedTypes,
            string[] requestedMetadata,
            Int32[] requestedCategory,
            long? lastStamp)
        {
            this.RequestedData = requestedTypes;
            this.RequestedMetadata = requestedMetadata;
            this.RequestedCategory = requestedCategory;
            this.LastStamp = lastStamp;
        }

        private Filter()
        {
        }

        internal static void WriteToStream(Filter filter, Stream stream)
        {
            using (TraceUtility.TraceMethod(Tracing.Source))
            {
                if (filter != null)
                {
                    byte[] buffer;
                    if (filter.LastStamp.HasValue)
                    {
                        buffer = BitConverter.GetBytes(filter.LastStamp.Value);
                        stream.Write(buffer, 0, buffer.Length);
                    }
                    if (filter.RequestedData != null)
                    {
                        foreach (DataType requestedDataType in filter.RequestedData)
                        {
                            buffer = BitConverter.GetBytes((int)requestedDataType);
                            stream.Write(buffer, 0, buffer.Length);
                        }
                    }
                    if (filter.RequestedMetadata != null)
                    {
                        foreach (string s in filter.RequestedMetadata)
                        {
                            buffer = Encoding.Unicode.GetBytes(s);
                            stream.Write(buffer, 0, buffer.Length);
                        }
                    }
                    if (filter.RequestedCategory != null)
                    {
                        foreach (Int32 rc in filter.RequestedCategory)
                        {
                            buffer = BitConverter.GetBytes(rc);
                            stream.Write(buffer, 0, buffer.Length);
                        }
                    }
                }
            }
        }

        #endregion        
    }

    [DataContract]
    [Serializable]
    public class LookupRequest
    {
        [DataMember(Name = "Type")]
        public uint Type { get; set; }
        [DataMember(Name = "SearchableFields")]
        public List<Metadata> SearchableFields { get; set; }

        #region Non-DataContract code

        // This is to mark the LookupRequest as an override request so that 
        // we can make decisions on it through out the service code
        public bool OverrideRequest { get; set; }

        public LookupRequest(uint type, IEnumerable<Metadata> searchableFields)
        {
            this.Type = type;
            if (searchableFields != null)
            {
                this.SearchableFields = new List<Metadata>(searchableFields);
            }
        }

        //private LookupRequest()
        //{
        //}

        //
        // TODO: if we get data from db, sync it with whatever is here;
        //
        /// <summary>
        ///     Determines if given lookup request is a valid request
        /// </summary>
        /// <param name="lookupRequest">
        ///     Lookup request to check for validity
        /// </param>
        /// <returns>
        ///     True if the lookup request is valid; otherwise, false.
        /// </returns>
        public static uint IsValidRequest(LookupRequest lookupRequest)
        {
            if (lookupRequest == null)
            {
                TraceUtility.TraceWarning(Tracing.Source, "Invalid request: null LookupRequest");
                return StatusCodes.NullLookupRequest;
            }

            if (lookupRequest.SearchableFields == null || lookupRequest.SearchableFields.Count == 0)
            {
                TraceUtility.TraceWarning(
                    Tracing.Source,
                    "Invalid request: LookupRequest has no SearchableFields.");
                return StatusCodes.NullSearchableMetadata;
            }
            else
            {
                List<string> foundFields = new List<string>();
                foreach (Metadata metadata in lookupRequest.SearchableFields)
                {
                    if (metadata == null)
                    {
                        TraceUtility.TraceWarning(
                            Tracing.Source,
                            "Invalid request: found null searchable field.");
                        return StatusCodes.NullSearchableMetadata;
                    }

                    foreach (string foundField in foundFields)
                    {
                        if (string.Equals(foundField, metadata.Name, StringComparison.OrdinalIgnoreCase))
                        {
                            TraceUtility.TraceWarning(
                                Tracing.Source,
                                "Invalid request: found more than one searchable field named {0}",
                                metadata.Name);
                            return StatusCodes.DuplicateSearchableMetadata;
                        }
                    }

                    foundFields.Add(metadata.Name);

                    string metaDataValue = metadata.Value as string;

                    if (!string.IsNullOrEmpty(metaDataValue))
                    {
                        //
                        // Enforce the custom encoding policy on the searchable metadata
                        //
                        if (!string.Equals(
                                metaDataValue,
                                XmlEncoder.Encode(XmlEncoder.Decode(metaDataValue)),
                                StringComparison.OrdinalIgnoreCase))
                        {
                            TraceUtility.TraceWarning(
                                Tracing.Source,
                                "Invalid request: metadata not properly encoded {0}",
                                metaDataValue);
                            return StatusCodes.InvalidSearchableMetadataEncode;
                        }
                    }
                }

                return StatusCodes.OK;
            }
        }

        public bool HasDataBlock(out byte[] datablock)
        {

            // SearchableFields will be null if this is a Guid search
            if (this.SearchableFields == null)
            {
                datablock = null;
                return false;
            }

            foreach (Metadata metadata in this.SearchableFields)
            {
                if (metadata.Name.Equals("DataBlock", StringComparison.InvariantCultureIgnoreCase))
                {
                    datablock = (byte[])metadata.Value;
                    return true;
                }
            }
            datablock = null;
            return false;
        }

        #endregion
    }

    [DataContract]
    [Serializable]
    public class ListingRequest
    {
        [DataMember(Name = "Type")]
        private uint m_parentObjectType;
        [DataMember(Name = "Guid")]
        private Guid? m_guid;
        [DataMember(Name = "ReturnObjectType")]
        private uint m_returnObjectType;
        [DataMember(Name = "Filter")]
        private Filter m_filter;
        [DataMember(Name = "LastStamp")]
        private long? m_lastStamp;

        #region Non-DataContract code

        public ListingRequest(
            uint parentObjectType,
            uint returnObjectType,
            Guid? guid,
            Filter filter,
            long? lastStamp)
        {
            this.Type = parentObjectType;
            this.ReturnObjectType = returnObjectType;
            this.Filter = filter;
            this.Guid = guid;
            this.LastStamp = lastStamp;
        }

        //private ListingRequest()
        //{
        //}

        //
        // TODO: if we get data from db, sync it with whatever is here;
        //
        /// <summary>
        ///     Determines if given lookup request is a valid request
        /// </summary>
        /// <param name="lookupRequest">
        ///     Lookup request to check for validity
        /// </param>
        /// <returns>
        ///     True if the lookup request is valid; otherwise, false.
        /// </returns>
        public static uint IsValidRequest(ListingRequest listingRequest)
        {
            if (listingRequest == null)
            {
                TraceUtility.TraceWarning(
                    Tracing.Source,
                    "Invalid request: null ListingRequest");
                return StatusCodes.NullListingRequest;
            }

            return Filter.IsValidRequest(listingRequest.Filter);

            //
            // TODO: check types if they match - should get this info from db what is compatible
            //
        }

        public Filter Filter
        {
            get
            {
                return m_filter;
            }
            set
            {
                m_filter = value;
            }
        }

        public uint ReturnObjectType
        {
            get
            {
                return m_returnObjectType;
            }
            set
            {
                m_returnObjectType = value;
            }
        }

        internal static void WriteToStream(ListingRequest request, Stream stream)
        {
            using (TraceUtility.TraceMethod(Tracing.Source))
            {
                if (request != null)
                {
                    byte[] buffer;
                    if (request.Guid.HasValue)
                    {
                        buffer = request.Guid.Value.ToByteArray();
                        stream.Write(buffer, 0, buffer.Length);
                    }
                    buffer = BitConverter.GetBytes(request.Type);
                    stream.Write(buffer, 0, buffer.Length);
                    buffer = BitConverter.GetBytes((request.ReturnObjectType));
                    stream.Write(buffer, 0, buffer.Length);
                    Filter.WriteToStream(request.Filter, stream);
                }
            }
        }

        public uint Type
        {
            get
            {
                return m_parentObjectType;
            }
            set
            {
                m_parentObjectType = value;
            }
        }

        public Guid? Guid
        {
            get
            {
                return m_guid;
            }
            set
            {
                m_guid = value;
            }
        }

        public long? LastStamp
        {
            get
            {
                return m_lastStamp;
            }
            set
            {
                m_lastStamp = value;
            }
        }

        #endregion
    }

    #endregion

    #region Response Classes

    [DataContract]
    [Serializable]
    public class ListingToken
    {
        [DataMember]
        public byte[] Data;

        #region Non-DataContract code

        public const ushort CurrentVersion = 0x0110;

        public const int VersionNumberOffset = 0;
        public const int LastReturnedStampFieldOffset = VersionNumberOffset + sizeof(ushort);
        public const int LastReturnedRequestNumberFieldOffset = LastReturnedStampFieldOffset + sizeof(long);
        public const int CheckSumFieldOffset = LastReturnedRequestNumberFieldOffset + sizeof(uint);

        public const int SizeOfListingToken = CheckSumFieldOffset + sizeof(long);

        private static byte[] s_rcKey = new byte[] { 0x41, 0x64, 0x61, 0xC5, 0x9B, 0x20, 0x57, 0x61 };
        private static byte[] s_rcIV = new byte[] { 0x72, 0x64, 0x79, 0xC5, 0x84, 0x73, 0x6B, 0x69 };

        public ListingToken(ListingToken original)
        {
            if (original == null)
            {
                throw new ArgumentNullException("original");
            }
            if (original.Data != null)
            {
                this.Data = (byte[])original.Data.Clone();
            }
        }

        public ListingToken(ListingRequest[] requests)
            : this()
        {
            Array.Copy(
                BitConverter.GetBytes(ComputeCheckSum(requests)),
                0,
                Data,
                CheckSumFieldOffset,
                sizeof(long));

            Array.Copy(
                BitConverter.GetBytes(CurrentVersion),
                0,
                Data,
                VersionNumberOffset,
                sizeof(ushort));
        }

        private ListingToken()
        {
            Data = new byte[]
            {
                // Version (ushort)
                0xFF, 0xFF,
                // Last returned serial number generated by the Pivot DB (long)
                0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
                // Last returned Request Number (uint)
                0x00, 0x00, 0x00, 0x00, 
                // Object Checksum (long)
                0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
            };
        }

        //public void SetLastRequestToken(
        //    long lastReturnedStamp,
        //    uint lastReturnedRequestNumber)
        //{
        //    Array.Copy(
        //        BitConverter.GetBytes(lastReturnedStamp),
        //        0,
        //        Data,
        //        LastReturnedStampFieldOffset,
        //        sizeof(long));

        //    Array.Copy(
        //        BitConverter.GetBytes(lastReturnedRequestNumber),
        //        0,
        //        Data,
        //        LastReturnedRequestNumberFieldOffset,
        //        sizeof(uint));
        //}

        public long? LastReturnedStamp
        {
            get
            {
                if (Data == null)
                {
                    return null;
                }
                try
                {
                    long stamp = BitConverter.ToInt64(Data, LastReturnedStampFieldOffset);

                    if (stamp < 0)
                    {
                        return 0;
                    }

                    return stamp;
                }
                catch (ArgumentOutOfRangeException)
                {
                    return null;
                }
            }
        }

        //public uint? LastReturnedRequestNumber
        //{
        //    get
        //    {
        //        if (Data == null)
        //        {
        //            return null;
        //        }

        //        try
        //        {
        //            return BitConverter.ToUInt32(Data, LastReturnedRequestNumberFieldOffset);
        //        }
        //        catch (ArgumentOutOfRangeException)
        //        {
        //            return null;
        //        }
        //    }
        //}

        public long? CheckSum
        {
            get
            {
                if (Data == null)
                {
                    return null;
                }

                try
                {
                    return BitConverter.ToInt64(Data, CheckSumFieldOffset);
                }
                catch (ArgumentOutOfRangeException)
                {
                    return null;
                }
            }
        }

        public static bool IsNew(ListingToken listingToken)
        {
            return listingToken == null || listingToken.Data == null;
        }

        public static uint IsValidRequest(ListingToken listingToken, ListingRequest[] requests)
        {
            if (IsNew(listingToken))
            {
                return StatusCodes.OK;
            }

            //
            // check size of TokenData etc, the version..
            //            

            if (listingToken.Data.Length != SizeOfListingToken)
            {
                TraceUtility.TraceWarning(
                    Tracing.Source,
                    "Invalid request: ListingToken's Data field length {0} differs from expected {1}.",
                    listingToken.Data.Length,
                    SizeOfListingToken);
                return StatusCodes.InvalidListingTokenSize;
            }

            if (listingToken.Version != CurrentVersion)
            {
                TraceUtility.TraceWarning(
                    Tracing.Source,
                    "Invalid request: ListingToken's Version {0} differs from expected {1}.",
                    listingToken.Version,
                    CurrentVersion);
                return StatusCodes.InvalidListingTokenVersion;
            }

            long checkSum = ComputeCheckSum(requests);

            if (listingToken.CheckSum != checkSum)
            {
                TraceUtility.TraceWarning(
                    Tracing.Source,
                    "Invalid request: ListingToken's CheckSum {0} differs from expected {1}.",
                    listingToken.CheckSum,
                    checkSum);
                return StatusCodes.InvalidListingTokenChecksum;
            }

            return StatusCodes.OK;
        }

        private static long ComputeCheckSum(ListingRequest[] requests)
        {
            long checkSum = 0;
            if (requests != null)
            {
                using (MemoryStream ms = new MemoryStream())
                {
                    using (RC2 rc2 = RC2.Create())
                    {
                        using (CryptoStream cs = new CryptoStream(
                            ms,
                            rc2.CreateEncryptor(s_rcKey, s_rcIV),
                            CryptoStreamMode.Write))
                        {
                            foreach (ListingRequest request in requests)
                            {
                                ListingRequest.WriteToStream(request, cs);
                            }

                            cs.FlushFinalBlock();

                            ms.Position = 0;
                            using (MD5 md5 = MD5.Create())
                            {
                                byte[] hash = md5.ComputeHash(ms);
                                checkSum = BitConverter.ToInt64(hash, 0);
                            }
                        }
                    }
                }
            }
            return checkSum;
        }

        private ushort Version
        {
            get
            {
                if (Data == null)
                {
                    return 0;
                }

                try
                {
                    return BitConverter.ToUInt16(Data, VersionNumberOffset);
                }
                catch (ArgumentOutOfRangeException)
                {
                    return 0;
                }
            }
        }

        #endregion
    }
    
    [DataContract]
    [Serializable]
    public class LookupResponse
    {
        [DataMember(Name = "Status")]
        public uint Status { get; set; }
        [DataMember(Name = "SeqNumber")]
        public uint SequenceNumber { get; set; }
        [DataMember(Name = "ExpirationMins")]
        public uint? ExpirationMins { get; set; }
        [DataMember(Name = "Matches")]
        public ICollection<ReputationObject> Matches { get; set; }

        #region Non-DataContract code

        /// <summary>
        ///     Initializes new LookupResponse object, representing response to a lookup request
        /// </summary>
        /// <param name="status">
        ///     Status of the response
        /// </param>
        /// <param name="sequenceNumber">
        ///     Sequence number of the response (the same as index of corresponding LookupRequest in the request array)
        /// </param>
        /// <param name="latestEntryDate">
        ///     The date of the latest entry among matches. Providing this date next time will result
        ///     in returing just data about changed objects
        /// </param>
        /// <param name="matches">
        ///     List of objects matched by the corresponding request.
        /// </param>
        public LookupResponse(
            uint status,
            uint sequenceNumber,
            uint? expirationMins,
            ICollection<ReputationObject> matches)
        {
            this.Status = status;
            this.SequenceNumber = sequenceNumber;
            this.ExpirationMins = expirationMins;
            this.Matches = matches;
        }
        #endregion

    }

    [DataContract]
    [Serializable]
    public class ListingResponse
    {
        [DataMember(Name = "Status")]
        public uint Status { get; set; }
        [DataMember(Name = "SeqNumber")]
        public uint SequenceNumber { get; set; }
        [DataMember(Name = "LatestStamp")]
        public long? LatestEntryStamp { get; set; }
        [DataMember(Name = "ExpirationMins")]
        public uint? ExpirationMins { get; set; }
        [DataMember(Name = "MatchedObject")]
        public ReputationObject MatchedObject { get; set; }
        [DataMember(Name = "RelatedObjects")]
        private List<ReputationObject> m_relatedObjects;

        #region Non-DataContract code

        /// <summary>
        ///     Initializes new LookupResponse object, representing response to a lookup request
        /// </summary>
        /// <param name="status">
        ///     Status of the response
        /// </param>
        /// <param name="sequenceNumber">
        ///     Sequence number of the response (the same as index of corresponding LookupRequest in the request array)
        /// </param>
        /// <param name="latestEntryDate">
        ///     The date of the latest entry among matches. Providing this date next time will result
        ///     in returing just data about changed objects
        /// </param>
        /// <param name="matches">
        ///     List of objects matched by the corresponding request.
        /// </param>
        public ListingResponse(
            uint status,
            uint sequenceNumber,
            uint? expirationMins,
            long? latestStamp,
            ReputationObject matchedObject,
            ICollection<ReputationObject> relatedObjects)
        {
            this.Status = status;
            this.SequenceNumber = sequenceNumber;
            this.LatestEntryStamp = latestStamp;
            this.MatchedObject = matchedObject;
            this.ExpirationMins = expirationMins;

            this.RelatedObjects = relatedObjects;
        }

        public ICollection<ReputationObject> RelatedObjects
        {
            get
            {
                return this.m_relatedObjects;
            }
            set
            {
                if (value == null)
                {
                    this.m_relatedObjects = null;
                }
                else
                {
                    this.m_relatedObjects = new List<ReputationObject>(value);
                }
            }
        }

        #endregion

    }

    #endregion
}
